# -*- coding: utf-8 -*-

# $Id: templating.py 13 2012-09-03 17:19:27Z alexis.roda.villalonga@gmail.com $

"""Every template engine has its own API. An adapter API is defined in
order to isolate the skeleton machinery from the actual template
engine being used to render the template.

Skeletons expect that the engine adapter classes define two methods:

* __init__(input_encoding='utf-8', \**kwargs)

  The engine adapter constructor must accept only keyword
  arguments. At this point only one argument is defined:

  :param input_encoding: encoding used in the template.

* render(path, context)

  Renders the template given by the path ``path`` using the variables
  defined in ``context``. The template is expected to be encoded in
  the ``input_encoding`` encoding.

  :param path: path of the template

  :param context: dictionary mapping variables to values.

  :returns: the rendered template
  :rtype: an unicode string

The template engine adapter must take care of adapting exceptions
raised by the template engine. At this point two *translations* are
defined:

* if the engine raises an exception related to a variable lookup
  error, variable not found etc. it must be translated into a
  :class:`dja_skel.lib.errors.UndefinedVarError` exception.

* other exceptions should be translated into a
  :class:`dja_skel.lib.errors.TemplateError` exception.

In order to create an adapter a class implementing the API must be
defined and registered under the ``dja_skel.template_engine`` entry
point.
"""

import re
from dja_skel.lib import errors


class _Options(object):
    def __init__(self, **kw):
        self.__dict__.update(kw)


class Engine(object):
    """This class implements the adapter API. Template engine adapters
    are not required to inherit from this class. It just provides some
    common functionality to ease the creation of new adapters.

    Subclasses must override the :meth:`_render` method.
    """

    def __init__(self, **kw):
        """Stores the arguments in the ``opts`` attribute (an object).
        """
        kw.setdefault("input_encoding", "utf-8")
        self.opts = _Options(**kw)

    def render(self, path, context):
        """Render the template.

        This implementation reads the template from the filesystem,
        decodes it using the ``input_encoding`` argument specified in
        the constructor and finally calls the :meth:`_render` method.
        """
        template = self._load_template(path)
        template = template.decode(self.opts.input_encoding)
        return self._render(template, context, path)

    def _render(self, template, context, path):
        """Actual renderer.

        Called by :meth:`render()`. It receives the template as an
        unicode string and must return the result as unicode.

        :param template: unicode string with template contents.

        :param context: dictionary mapping variables to values.

        :param path: path of the template, mostly useful to report
          errors.

        :returns: the rendered template.
        :rtype: an unicode string.
        """
        raise NotImplementedError()

    def _load_template(self, path):
        """Load the template.
        """
        with file(path, "r") as f:
            return f.read()


class FormatEngine(Engine):
    """Engine adapter for the ``string.format()`` method.
    """
    def _render(self, template, context, path):
        try:
            return template.format(**context)
        except KeyError, exc:
            raise errors.UndefinedVarError(
                exc.args[0],
                path
                )


class CheetahEngine(Engine):
    """Engine adapter for cheetah templates.
    """

    _var_name_re = re.compile("'([_A-Za-z0-9]+)'")

    def _render(self, template, context, path):
        # the import goes here in order to avoid an ImportError when
        # cheetah is not installed and this engine isn't used
        from Cheetah.Template import Template
        from Cheetah.NameMapper import NotFound
        from Cheetah.Parser import ParseError

        try:
            t = Template(template, searchList=[context])
        except ParseError, exc:
            raise errors.TemplateError(exc.report(), path)
        try:
            return unicode(t)
        except NotFound, exc:
            raise errors.UndefinedVarError(
                self._extract_varname_from_exception(exc),
                path
                )

    def _extract_varname_from_exception(self, exc):
        m = self._var_name_re.search(exc.args[0])
        if m is not None:
            return m.groups()[0]
        # ugly, buy hopefully the user will be able to determine the
        # variable name
        return exc.args[0]

class DjangoEngine(Engine):
    """Engine adapter for django templates.
    """

    def _render(self, template, context, path):
        from django.template import Context
        from django.template import Template
        from django.template import TemplateEncodingError
        from django.template import TemplateSyntaxError
        from django.template import VariableDoesNotExist
        errs = ( TemplateEncodingError,
                 TemplateSyntaxError,
                 VariableDoesNotExist,
                 )
        try:
            t = Template(template)
        except errs as excpt:
            raise errors.TemplateError(excp.message, path)
        try:
            return t.render(Context(context))
        except errs as excp:
            raise errors.TemplateError(excp.message, path)
